/*
 * Copyright Hundsun Technologies Inc. All Rights Reserved.
 */

package miner

import (
	"encoding/hex"
	"errors"
	"fmt"
	"time"

	"github.com/golang/protobuf/proto"
	"hundsun.com/hsl/hschain/common/message"
	commtype "hundsun.com/hsl/hschain/common/types"
	"hundsun.com/hsl/hschain/common/util/pb"
	dposcomm "hundsun.com/hsl/hschain/consensus/impl/dpos/common"
	minercomm "hundsun.com/hsl/hschain/consensus/impl/dpos/miner/common"
	"hundsun.com/hsl/hschain/consensus/types"
	"hundsun.com/hsl/hschain/protos"
	pbdpos "hundsun.com/hsl/hschain/protos/consensus/dpos"
	pbledger "hundsun.com/hsl/hschain/protos/ledger"
	"hundsun.com/hsl/hschain/store/mq"
)

// Endorse 对区块进行背书
func (m *DposMiner) Endorse(originPeerID string, request *types.ConsensRelayMsg) error {
	log := m.Log
	var err error
	var block = &pbledger.Block{}
	err = proto.Unmarshal(request.Payload, block)
	if err != nil {
		sendRespErr := m.sendEndorsementResp(originPeerID, block, err)
		if sendRespErr != nil {
			log.Errorf("failed to send ed resp:%s", sendRespErr.Error())
		}
		return sendRespErr
	}

	blkLog := pb.GetBlockLog(log, block)
	blkLog.Infof("received ed request from peer: %s", originPeerID)
	if m.CurrentRoundBlockBuffer.IsEmpty() && !m.IsBlockSynchronized() {
		err = errors.New("cannot validate due to block not synchronized")
	}
	if err == nil {
		err = m.validateBlock(block)
	}
	if err == nil {
		blkLog.Info("ed passed")
		m.CurrentRoundBlockBuffer.Add(block, m.Log)
		isCreatedByBrotherMiner := m.BrotherMiners.IndexOf(block.Header.Signature.GetAddress()) > -1
		if m.TxMarkerLockHoldFlag.Load() && !isCreatedByBrotherMiner {
			txHashes := pb.GetTxHashesInBlock(block)
			blkLog.Debug("PreRemoveSpecifiedTransactions starts...")
			preRemoveErr := m.baseClient.PreRemoveSpecialTransactions(txHashes)
			if preRemoveErr != nil {
				blkLog.Warnf("PreRemoveSpecifiedTransactions failed: %s", preRemoveErr.Error())
			} else {
				blkLog.Debug("PreRemoveSpecifiedTransactions ends...")
			}
			m.GetClient().PutPreRemovedTxHashesIntoLocalCache(block.Hash(), txHashes, blkLog)
		}
	} else {
		blkLog.Errorf("failed to validate: %s", err)
	}
	blkLog.Debug("sending ed resp")
	err = m.sendEndorsementResp(originPeerID, block, err)
	if err != nil {
		blkLog.Errorf("failed to send ed resp:%s", err.Error())
	} else {
		blkLog.Infof("ed resp sent, targetPeerID: %s", originPeerID)
	}
	return nil
}

func (m *DposMiner) validateBlock(block *pbledger.Block) error {
	err := m.baseClient.ValidateBasicInfoOfBlock(block)
	if err != nil {
		return err
	}

	if time.Now().UnixNano() < block.Header.BlockTime {
		return errors.New("invalid blockTime")
	}

	lastCheckedBlockNumber := m.GetClient().GetLastBlockCheckedMessage().BlockNumber
	if block.Header.Number <= lastCheckedBlockNumber {
		return fmt.Errorf("blockNumber '%d' is smaller than block number '%d' in BlockCheckedMessage", block.Header.Number, lastCheckedBlockNumber)
	}

	blockCreator := block.Header.Signature.GetAddress()
	currentRound, blockCreatorIndex := minercomm.GetCurrentRoundMinerIndex(blockCreator)
	if blockCreatorIndex == -1 {
		return errors.New("blockCreator " + string(blockCreator) + " is not a miner of current round")
	}
	timeSliceStart, timeSliceEnd := dposcomm.CalculateTimeSlice(currentRound, blockCreatorIndex)
	if timeSliceStart > block.Header.BlockTime || timeSliceEnd <= block.Header.BlockTime {
		return fmt.Errorf("blockTime of block '%d' out of the time slice,expected:[%d,%d),actual:%d", block.Header.Number, timeSliceStart, timeSliceEnd, block.Header.BlockTime)
	}

	var txTotalSize int
	for _, tx := range block.Transactions {
		err = pb.ValidateTxExpiration(block.Header.BlockTime, tx)
		if err != nil {
			return err
		}
		txSize, err := pb.ValidateTxSize(tx)
		if err != nil {
			return err
		}
		txTotalSize += txSize
	}

	maxTxTotalSize := m.baseClient.CalcMaxTxsSizeInBlock()
	if txTotalSize > maxTxTotalSize {
		return fmt.Errorf("size of txs '%d' exceed limit '%d'", txTotalSize, maxTxTotalSize)
	}

	lastBlockNumber, lastBlockHash, lastBlockTime := m.getPreviousBlockForEndorsing(block.Header.Number)
	err = pb.ValidatePrevBlockInfoOfBlock(block, lastBlockTime, lastBlockNumber, lastBlockHash)
	if err != nil {
		return err
	}
	if !block.Verify() {
		return errors.New("bad block signature ")
	}

	checkBlockNumber, checkBlockEnabled := pb.GetCheckBlockNumber(block.Header.Number)
	if checkBlockEnabled {
		checkBlockExecMsg := m.baseClient.GetBlockExecutedMessage(checkBlockNumber, m.Log)
		if checkBlockExecMsg == nil {
			err = errors.New("cannot find check block info")
		} else {
			stateHash := protos.Hash(checkBlockExecMsg.StateHash)
			if !stateHash.Equals(block.Header.StateHashOfCheckBlock) {
				err = fmt.Errorf("unexpected check block hash,expected:%s,actual:%s", hex.EncodeToString(block.Header.StateHashOfCheckBlock), hex.EncodeToString(checkBlockExecMsg.StateHash))
			}
		}
	}
	return err
}

// getPreviousBlockForEndorsing 获取指定块的上一块信息（区块时间，区块号，区块哈希）用于背书
func (m *DposMiner) getPreviousBlockForEndorsing(blockNumber uint64) (lastBlockNumber uint64, lastBlockHash protos.Hash, lastBlockTime int64) {
	// 如果blockNumber不大于CurrentRoundBlocksStack栈顶的块，则出栈直到栈顶块小于指定块
	lastBlk := m.CurrentRoundBlockBuffer.PurgeBlockNumberEq(blockNumber, m.Log)
	if lastBlk == nil {
		lastBlockCheckedMsg := m.GetClient().GetLastBlockCheckedMessage()
		lastBlockTime = lastBlockCheckedMsg.BlockTime
		lastBlockNumber = lastBlockCheckedMsg.BlockNumber
		lastBlockHash = lastBlockCheckedMsg.BlockHash
		m.Log.Debugf("endorse with prev block[number='%d',parentHash=%s] in last BlockCheckedMessage", lastBlockNumber, hex.EncodeToString(lastBlockHash))
	} else {
		lastBlockTime = lastBlk.Header.BlockTime
		lastBlockNumber = lastBlk.Header.Number
		lastBlockHash = lastBlk.Hash()
		m.Log.Debugf("endorse with prev block[number='%d',parentHash=%s] in RoundBlocksBuffer", lastBlockNumber, hex.EncodeToString(lastBlockHash))
	}
	return
}

func (m *DposMiner) sendEndorsementReq(peerID string, blk *pbledger.Block) error {
	var reqMsg = &types.ConsensRelayMsg{}
	payload, err := proto.Marshal(blk)
	if err != nil {
		return err
	}

	reqMsg.MsgType = types.ConsensusEndorserMsg
	reqMsg.Payload = payload

	p2pMsg, err := message.EncodeMessage(peerID, reqMsg)
	if err != nil {
		return err
	}

	m.Log.Tracef("Send relay msg to p2p, targetMode: %s and targetPeer: %s", p2pMsg.TargetMod, p2pMsg.TargetPeerId)

	msg := m.GetClient().GetTopicClient().NewMessage(commtype.P2pModule, mq.ModeTopic.String(), commtype.EventConsensusRequestMsg, p2pMsg)
	err = m.GetClient().GetTopicClient().Send(msg, false)
	if err != nil {
		return err
	}

	return nil
}

func (m *DposMiner) sendEndorsementResp(originPeerID string, block *pbledger.Block, err error) error {
	if block == nil {
		block = &pbledger.Block{}
	}
	resp := &pbdpos.BlockEndorsementResponse{
		BlockNumber: getBlockNumber(block),
		BlockHash:   block.Hash(),
		Approved:    err == nil,
	}
	if err != nil {
		resp.ErrorMessage = err.Error()
	}
	resp.Sign(m.MinerAccount.GetKeyPair())

	payload, err := proto.Marshal(resp)
	if err != nil {
		return err
	}

	var respMsg = &types.ConsensRelayMsg{}
	respMsg.MsgType = types.ConsensusEndorserRespMsg
	respMsg.Payload = payload

	msg, err := message.EncodeMessage(originPeerID, respMsg)
	if err != nil {
		return err
	}

	m.Log.Infof("Relay response to p2p, targetMod: %s", msg.TargetMod)
	relayMsg := m.GetClient().GetTopicClient().NewMessage(commtype.P2pModule, mq.ModeTopic.String(), commtype.EventConsensusRequestMsg, msg)
	err = m.GetClient().GetTopicClient().Send(relayMsg, false)
	if err != nil {
		return err
	}
	return nil
}
